﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using YAFTS.Contracts;
using YAFTS.Utils;

namespace YAFTS.Core
{
	internal sealed class FileSystemIOObserver : IIOObserver
	{
		private readonly IConfiguration _configuration;
		private readonly List<string> _observedPaths = new List<string>();
		private readonly List<FileSystemWatcher> _watchers = new List<FileSystemWatcher>();

		public FileSystemIOObserver(IConfiguration configuration)
		{
			_configuration = configuration;
		}

		public event EventHandler<FileContentChangedArgs> FileContentChange;
		public event EventHandler<RenamedEventArgs> FileRenamed;

		private void OnFileRenamed(RenamedEventArgs e)
		{
			var handler = FileRenamed;
			if (handler != null)
			{
				handler(this, e);
			}
		}

		public bool CanObverve(string path)
		{
			return File.Exists(path) || Directory.Exists(path);
		}


		public void AddFolderToWatch([NotNull] string path, string fileMask, bool reqursive)
		{
			if (path == null)
			{
				throw new ArgumentNullException("path");
			}

			if (!Directory.Exists(path))
			{
				throw new DirectoryNotFoundException();
			}

			AddPathToObserve(path, path);

			EnumerateFiles(path, fileMask ?? _configuration.Filter).AsParallel().ForAll(
				file => OnFileContentChange(new FileContentChangedArgs(file, CreateStreamReader)));
		}

		public void AddFileToWatch(string path)
		{
			if (!File.Exists(path))
			{
				throw new FileNotFoundException();
			}

			var baseFolder = Path.GetDirectoryName(path);

			if (baseFolder == null)
			{
				return;
			}

			AddPathToObserve(path, baseFolder);

			OnFileContentChange(new FileContentChangedArgs(path, _ => new StringReader(path)));
		}

		public void RemoveFromWatch(string path)
		{
			var baseFolder = Path.GetDirectoryName(path);

			if (baseFolder == null)
			{
				return;
			}

			lock (_observedPaths)
			{
				_observedPaths.Remove(path);
				if (_observedPaths.Any(x => x.Contains(baseFolder)))
				{
					return;
				}

				lock (_watchers)
				{
					_watchers.FirstOrDefault(x => x.Path.Contains(baseFolder)).Do(x => _watchers.Remove(x));
				}
			}
		}

		private void OnFileContentChange(FileContentChangedArgs e)
		{
			var handler = FileContentChange;
			if (handler != null)
			{
				handler(this, e);
			}
		}

		private IEnumerable<string> EnumerateFiles(string path, string pattern)
		{
			IEnumerable<string> paths = null;
			try
			{
				paths = Directory.GetFiles(path, pattern);
			}
			catch (UnauthorizedAccessException)
			{
			}
			if (paths != null)
			{
				foreach (var file in paths)
				{
					yield return file;
				}
			}
			paths = null;
			try
			{
				paths = Directory.GetDirectories(path);
			}
			catch (UnauthorizedAccessException)
			{
			}

			if (paths != null)
			{
				foreach (var file in paths.SelectMany(x => EnumerateFiles(x, pattern)))
				{
					yield return file;
				}
			}
		}

		private TextReader CreateStreamReader(string path)
		{
			if (File.Exists(path))
			{
				return new StreamReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite),
				                        _configuration.DefaultEncoding);
			}
			return new StringReader(string.Empty);
		}

		private void AddPathToObserve(string path, string folder)
		{
			lock (_observedPaths)
			{
				lock (_watchers)
				{
					_watchers.FirstOrDefault(x => x.Path.Contains(folder)).Do(x => x.Path = folder)
						.Unless(() => _watchers.Add(CreateNewWatcher(folder)));
				}

				if (!_observedPaths.Contains(path))
				{
					_observedPaths.Add(path);
				}
			}
		}

		private void RemoveWatcher(FileSystemWatcher watcher)
		{
			watcher.EnableRaisingEvents = false;
			watcher.Deleted -= WatcherOnDeleted;
			watcher.Changed -= WatcherOnChanged;
			watcher.Renamed -= WatcherOnRenamed;
			lock (_watchers)
			{
				_watchers.Remove(watcher);
			}
		}

		private FileSystemWatcher CreateNewWatcher(string folder)
		{
			var watcher = new FileSystemWatcher(folder);
			watcher.Deleted += WatcherOnDeleted;
			watcher.Changed += WatcherOnChanged;
			watcher.Renamed += WatcherOnRenamed;
			watcher.EnableRaisingEvents = true;
			watcher.Filter = _configuration.Filter;
			return watcher;
		}

		private void WatcherOnRenamed(object sender, RenamedEventArgs renamedEventArgs)
		{
			OnFileRenamed(renamedEventArgs);
		}

		private void WatcherOnChanged(object sender, FileSystemEventArgs fileSystemEventArgs)
		{
			var filePath = fileSystemEventArgs.FullPath;
			var runUpdate = false;
			lock (_observedPaths)
			{
				if (_observedPaths.Any(filePath.Contains))
				{
					runUpdate = true;
				}
			}

			if (runUpdate && File.Exists(filePath))
			{
				OnFileContentChange(new FileContentChangedArgs(filePath, CreateStreamReader));
			}
		}

		private void WatcherOnDeleted(object sender, FileSystemEventArgs fileSystemEventArgs)
		{
			lock (_observedPaths)
			{
				if (_observedPaths.Contains(fileSystemEventArgs.FullPath))
				{
					(sender as FileSystemWatcher).If(x => !_observedPaths.Any(path => path.Contains(x.Path))).Do(RemoveWatcher);
				}
			}

			OnFileContentChange(new FileContentChangedArgs(fileSystemEventArgs.FullPath, path => new StringReader(string.Empty)));
		}
	}
}